home *** CD-ROM | disk | FTP | other *** search
-
-
-
- The PC Assembler Tutor 148
- ______________________
-
-
- THE STACK
-
- Up to this time we have used the stack for temporary storage. If
- you want to temporarily save either a register or a value in
- memory, you push it:
-
- push ax
- push variable1
-
- and if you want to get them back you pop them:
-
- pop variable1
- pop ax
-
- This is always a word (2 bytes) at a time. When you pop the
- stack, the 8086 gives you back the words in reverse order. Thus
- if you push the following:
-
- push variable1
- push variable2
- push variable3
- push variable4
- push variable5
-
- then in order to get the data back in the same place, you need to
- pop in this order:
-
- pop variable5
- pop variable4
- pop variable3
- pop variable2
- pop variable1
-
- It pops the last thing that was pushed that hasn't been popped
- yet.
-
- Nothing has been said about where the stack is or how it
- operates. It's time to change that. When the operating system
- starts a program, it looks for a stack segment. If the stack
- segment has been properly defined, the operating system puts the
- stack segment's segment address in SS (the stack segment
- register) and sets SP (the stack pointer) to point to the first
- byte AFTER the end of the stack segment. Exactly where this is
- depends on how large you have defined your stack segment. SS and
- SP are set, and there is nothing on the stack.
-
- When you push something:
-
- push dx
-
- the 8086 subtracts 2 from SP (making one word of space) and puts
- that thing at the new address in SP. SP contains the address of
-
- ______________________
-
- The PC Assembler Tutor - Copyright (C) 1989 Chuck Nelson
-
-
-
-
- Chapter 15 - Subroutines 149
- ________________________
-
- the last thing pushed.
-
- This means that SP is decreasing, and the stack segment is
- filling up from back to front. In the topsy-turvy world of
- stacks, when you put things on the stack, the stack grows
- downward. What makes things especially confusing is that many
- book writers will picture a stack:
-
- variable1
- variable2
- ax
- dx
-
- and not bother to tell you whether the stack is growing upwards
- or downwards or where the stack top is. In this book, the stack
- TOP will always be visually on the BOTTOM. High addresses will be
- visually up and low addresses will be visually down. You need to
- get used to SP decreasing as the stack gets larger, and this is
- the easiest way to do it. So, if you have the instructions:
-
- push ax
- push variable1
- push si
- push di
-
- after these instructions, the stack will look like this:
-
- VALUE ADDRESS
-
- ax sp + 6
- variable1 sp + 4
- si sp + 2
- sp -> di sp + 0
-
- When you pop a value, the 8086 moves the word (2 bytes) at SP to
- the appropriate location and INCREMENTS SP by 2.
-
- pop di
-
- You would now have:
-
- VALUE ADDRESS
-
- ax sp + 4
- variable1 sp + 2
- sp -> si sp + 0
-
- As long as you are just using PUSH and POP, this is entirely self
- regulating. SS is set, and SP is modified by the 8086 without you
- doing anything. It is now time to get more sophisticated.
-
- In our C example:
-
- my_procedure (variable1, variable2, variable3) ;
-
- we generated the code:
-
-
-
-
-
- The PC Assembler Tutor 150
- ______________________
-
- push variable3
- push variable2
- push variable1
- call my_procedure
-
- What will the stack look like upon entry to my_procedure? That
- depends on whether my_procedure is a near procedure or a far
- procedure. If it is a near procedure, you will have:
-
-
- VALUE ADDRESS
-
- variable3 sp + 6
- variable2 sp + 4
- variable1 sp + 2
- sp -> old IP sp + 0
-
- If it is a far procedure, you will have:
-
- VALUE ADDRESS
-
- variable3 sp + 8
- variable2 sp + 6
- variable1 sp + 4
- old CS sp + 2
- sp -> old IP sp + 0
-
- Therefore, the variables are in different places relative to SP
- depending on whether it is a near or a far procedure. All
- examples will be with near procedures, but they are all valid for
- far procedures if you adjust for having the old CS on the stack.
-
- How do we access these variables? By using a pointer. We could
- use BX, SI or DI, but they have DS, not SS as their natural
- segment register. The only pointer with SS as the natural segment
- register is BP, the base pointer. Since we are going to use BP,
- we need to push its current value in order to save it:
-
- push bp
-
- The stack now looks like this:
-
- VALUE ADDRESS
-
- variable3 sp + 8
- variable2 sp + 6
- variable1 sp + 4
- old IP sp + 2
- sp -> old bp sp + 0
-
- This is the standard way to do it and this is what the stack
- always looks like if you follow the standard method. The standard
- code for setting up the stack for access is:
-
- push bp
- mov bp, sp
-
-
-
-
-
- Chapter 15 - Subroutines 151
- ________________________
-
- We give BP the same value as SP, so BP also points to the top of
- the stack and we use BP instead of SP. We now have:
-
- VALUE ADDRESS
-
- variable3 bp + 8
- variable2 bp + 6
- variable1 bp + 4
- old IP bp + 2
- bp -> old bp bp + 0
-
- Now, if you want to push and pop things, you can do it to your
- heart's content. BP will always point to the set of data that you
- want to work with. Let's take the average of the three variables,
- and print it.
-
- mov ax, [bp+4] ; add the three numbers
- add ax, [bp+6]
- add ax, [bp+8]
- mov dx, 0 ; prepare dx for division
- mov bx, 3 ; unsigned divide by 3
- div bx
- call print_unsigned ; result is in ax
-
- We are using AX, BX, and DX, so we need to push them before doing
- this:
-
- push ax
- push bx
- push dx
-
- After we are done we need to (1) pop the registers and (2)
- restore BP. This is also a pop.
-
- pop dx
- pop bx
- pop ax
- pop bp
- ret
-
- The whole subprogram now looks like this
-
- ;-----
- my_procedure proc near
-
- push bp ; set up base pointer
- mov bp, sp
- push ax ; push registers
- push bx
- push dx
- mov ax, [bp+4] ; add the three numbers
- add ax, [bp+6]
- add ax, [bp+8]
- mov dx, 0 ; prepare dx for division
- mov bx, 3 ; unsigned divide by 3
- div bx
- call print_unsigned ; result is in ax
-
-
-
-
- The PC Assembler Tutor 152
- ______________________
-
- pop dx ; pop registers
- pop bx
- pop ax
- pop bp ; restore old base pointer
- ret
-
- my_procedure endp
- ;------
-
- There is only one more improvement to make. If you look at the
- code, it is not clear what [bp+4] refers to. We know where it is,
- but what is it? Therefore, we will always use EQU statements to
- give names to our stack variables. It will be clearer, and if you
- need to change the code, it is much easier to change the EQU
- definition than to change the stack references in the code. As
- usual, we follow the C convention and put EQU names in capital
- letters.
-
- ;-----
- my_procedure proc near
-
- VAR1 EQU [bp+4]
- VAR2 EQU [bp+6]
- VAR3 EQU [bp+8]
-
- push bp ; set up base pointer
- mov bp, sp
- push ax ; push registers
- push bx
- push dx
- mov ax, VAR1 ; add the three numbers
- add ax, VAR2
- add ax, VAR3
- mov dx, 0 ; prepare dx for division
- mov bx, 3 ; divide by 3
- div bx
- call print_unsigned ; result is in ax
- pop dx ; pop registers
- pop bx
- pop ax
- pop bp ; restore old base pointer
- ret
-
- my_procedure endp
- ;------
-
- This is a simple example, so it doesn't look that important to
- use the EQU statements. Just wait till you have more complex
- subroutines. By the way, this program does no error checking. (If
- the sum is > 65535 it will give the wrong answer).
-
- There is still one thing to do. When we called the subroutine, we
- pushed the variables on the stack:
-
- push variable3
- push variable2
- push variable1
-
-
-
-
- Chapter 15 - Subroutines 153
- ________________________
-
- call my_procedure
-
- We now want to take them off. Do we need to pop them? No, this is
- trash so they go into the Great Bit Bucket. There are two ways of
- doing this, and this is language dependent.{1} In C, it is the
- STANDARD that the calling routine takes them off, and it is done
- this way:
-
- push variable3
- push variable2
- push variable1
- call my_procedure
- add sp, 6 ; 3 variables = 6 bytes
-
- we simply INCREASE sp by the number of bytes that we pushed on
- the stack. Whoof, they're gone.
-
- If you use PASCAL or FORTRAN, then the CALLED routine must take
- the variables off the stack on return. How does it do that? There
- is yet another type of return statement:
-
- ret (6) ; 3 variables = 6 bytes {2}
-
- causes the 8086 to increase sp by 6 as the last thing it does
- before returning from the subroutine. Which method you use is
- decided by which high-level language you are using.
-
-
- MACHINE CODE ASSEMBLER INSTRUCTIONS
-
- ;-----
- far_routine proc far
- CA 001A ret (26) ; hex 1A
- CB ret
- far_routine endp
- ;-----
-
- ;-----
- near_routine proc near
- C2 002C ret (44) ; hex 2C
- C3 ret
- near_routine endp
- ;-----
-
- Here are the four different types of returns along with the
- machine code. Notice that the returns which increment the stack
- have the increment count coded in the machine code.
-
-
- You may have noticed that even in this first subroutine, pushing
- and popping the registers takes a lot of space. It is fairly
- ____________________
-
- 1 And a major reason that is a real pain in keester to have
- multi-language programs.
-
- 2 The parentheses are not necessary.
-
-
-
-
- The PC Assembler Tutor 154
- ______________________
-
- normal to use 6 registers in a subroutine. This means that you
- will need to write:
-
- push ax
- push bx
- push cx
- push dx
- push si
- push di
-
- at the beginning of the subroutine and:
-
- pop di
- pop si
- pop dx
- pop cx
- pop bx
- pop ax
-
- before returning. This is a lot of space and it gets boring.
- Also, you have to remember to pop in the exact reverse order or
- you will screw things up. Fortunately we have two macros to help
- us. The file PUSHREGS.MAC has two macros, one called PUSHREGS
- and the other called POPREGS.
-
- A macro is a set of directions for generating additional
- assembler code before the file is assembled. That's why it is
- called the Microsoft Macro Assembler. You include \pushregs.mac
- at the beginning of the file, and then everytime the assembler
- sees the word PUSHREGS followed by register names it generates
- push instructions. Every time the assembler sees POPREGS followed
- by register names, it generates pop instructions. It generates
- actual text which is assembled later.
-
- The form for generating those push instructions above is:
-
- PUSHREGS ax, bx, cx, dx, si, di
-
- the word PUSHREGS followed by the registers separated by commas.
- (Make sure there is no comma after the last register). This must
- all be on one line. PUSHREGS pushes the registers in left to
- right order.
-
- The form for generating those pop instructions above is
-
- POPREGS ax, bx, cx, dx, si, di
-
- The registers will be popped in the REVERSE order to the way they
- are listed on the line, that is, in RIGHT TO LEFT order.
-
- Notice that the order of registers is the same for both PUSHREGS
- and POPREGS. This is so that you may write the push part:
-
- PUSHREGS ax, bx, cx, dx, si, di
-
- and then use your word processor to copy the line to the end of
- the subroutine, changing PUSHREGS to POPREGS. This insures that
-
-
-
-
- Chapter 15 - Subroutines 155
- ________________________
-
- the pushes and pops will be in exact reverse order. It saves
- space and time, and it generates exactly the same code as if you
- had written all those pushes and pops in the code. Whenever we
- have subroutines in the future, we will always use it.
-
-
- MOVING A STRING
-
- As a final example, we will create a subroutine that moves a
- Pascal string from one place to another. We'll assume that both
- strings are in the current DS, so no segments need to be changed.
-
- move_string ( from_string, to_string ) ;
-
- where from_string and to_string are the ADDRESSES of the strings.
- The code generated by the Pascal compiler will be:
-
- mov ax, offset from_string
- push ax
- mov ax, offset to_string
- push ax
- call move_string
- ; this is Pascal, so the CALLED subroutine must
- ; get rid of the variables on the stack.
-
- Notice that Pascal pushes this data in left to right order,
- exactly the opposite of how C would handle it. After setting up
- BP, we have:
-
- from_string offset bp + 6
- to_string offset bp + 4
- old IP bp + 2
- bp -> old bp bp + 0
-
- Before coding this, you need to know the structure of a Pascal
- text string. The first byte (string[0]) is not text, but the text
- count. The second byte is the first piece of text. This means two
- things. First, the maximum string size in Pascal is 255, the
- largest count that will fit in one byte. Second, you need to move
- 'count + 1' bytes. 'count' is how many text bytes there are, but
- then you need to move the count itself. If the string is empty
- (count = 0) you need to move 1 byte, the count byte. Here's the
- code
-
- ; - - - - -
- move_string proc near
-
- FROM_ADDRESS EQU [bp + 6]
- TO_ADDRESS EQU [bp + 4]
-
- push bp ; set up bp
- mov bp, sp
- PUSHREGS ax, cx, si, di
-
- mov si, FROM_ADDRESS ; source
- mov di, TO_ADDRESS ; destination
- sub cx, cx ; zero cx
-
-
-
-
- The PC Assembler Tutor 156
- ______________________
-
- mov cl, [si] ; text byte count of source
- inc cl ; add 1 for byte count itself
-
- move_loop:
- mov al, [si] ; source to al
- mov [di], al ; al to destination
- inc si ; move pointers to next byte
- inc di
- loop move_loop
-
- POPREGS ax, cx, si, di
- pop bp
- ret (4) ; Pascal, so pop offsets.
-
- move_string endp
- ; - - - - - - -
-
- We still have some more to do, and we'll do it in part three of
- the chapter.
-
-